package org.study.minio.boot.service.impl;

import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Maps;
import io.minio.StatObjectResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.io.IOUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.study.minio.boot.config.MinioConstant.StatusCode;
import org.study.minio.boot.service.MinioService;
import org.study.minio.boot.util.MinioUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 作用描述
 *
 * @author doveylovey
 * @version v1.0.0
 * @email 1135782208@qq.com
 * @date 2023年12月12日
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class MinioServiceImpl implements MinioService {
    // 存储视频的元数据列表
    private static final String OBJECT_INFO_LIST = "com:minio:media:objectList";
    // 已上传文件的md5列表
    private static final String MD5_KEY = "com:minio:file:md5List";

    private final RedisTemplate redisTemplate;

    @Override
    public Map<String, Object> uploadCheck(String md5) {
        Map<String, Object> resultMap = Maps.newHashMap();
        if (ObjectUtils.isEmpty(md5)) {
            resultMap.put("status", StatusCode.PARAM_ERROR.getCode());
            return resultMap;
        }
        // 从Redis中查询文件是否存在
        Object url = redisTemplate.boundHashOps(MD5_KEY).get(md5);
        if (ObjectUtils.isEmpty(url)) {
            resultMap.put("status", StatusCode.NOT_FOUND.getCode());
            return resultMap;
        }
        resultMap.put("status", StatusCode.SUCCESS.getCode());
        resultMap.put("url", url);
        return resultMap;
    }

    @Override
    public Map<String, Object> upload(HttpServletRequest request) throws Exception {
        Map<String, Object> retMap = Maps.newHashMap();
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
        // 获得文件分片数据
        MultipartFile file = multipartRequest.getFile("data");
        // 上传过程中出现异常，状态码设置为50000
        if (file == null || file.isEmpty()) {
            retMap.put("status", StatusCode.FAILURE.getCode());
            retMap.put("msg", "分片文件不能为空");
            return retMap;
        }
        // 分片第几片
        int index = Integer.parseInt(multipartRequest.getParameter("index"));
        // 总片数
        int total = Integer.parseInt(multipartRequest.getParameter("total"));
        // 获取文件名
        String fileName = multipartRequest.getParameter("name");
        String md5 = multipartRequest.getParameter("md5");
        // 创建文件桶
        MinioUtils.createBucket(md5);
        String objectName = String.valueOf(index);
        log.info("当前分片文件信息：index={}，total={}，fileName={}，md5={}，objectName={}", index, total, fileName, md5, objectName);

        String url = MinioUtils.uploadFile(md5, objectName, file.getInputStream());
        if (index < total) {
            // 当不是最后一片时，上传返回的状态码为20001
            retMap.put("status", StatusCode.ALONE_CHUNK_UPLOAD_SUCCESS.getCode());
            retMap.put("msg", "当前分片文件上传成功");
        } else {
            // 为最后一片时状态码为20002
            retMap.put("status", StatusCode.ALL_CHUNK_UPLOAD_SUCCESS.getCode());
            retMap.put("msg", "最后一个分片文件上传成功");
        }
        retMap.put("url", url);
        return retMap;
    }

    @Override
    public Map<String, Object> merge(String targetBucketName, int shardCount, String fileName, String md5, String fileType, long fileSize) throws Exception {
        Map<String, Object> retMap = Maps.newHashMap();
        // 查询片数据
        List<String> objectNameList = MinioUtils.listObjectsName(md5, false);
        if (shardCount != objectNameList.size()) {
            retMap.put("status", StatusCode.FAILURE.getCode());
            retMap.put("msg", "分片缺失，请重新上传");
        } else {
            // 开始合并请求
            String objectName = md5 + "." + StringUtils.getFilenameExtension(fileName);
            String url = MinioUtils.composeObject(md5, targetBucketName, objectName);
            log.info("桶：{}中的分片文件，已经在桶：{}合并成功，合并后的文件名称：{}", md5, targetBucketName, objectName);
            // 合并成功之后删除对应的临时桶
            MinioUtils.removeBucketForce(md5);
            retMap.put("status", StatusCode.SUCCESS.getCode());
            retMap.put("url", url);
            log.info("删除桶 {} 成功", md5);
        }
        return retMap;
    }

    @Override
    public void videoPlay1(HttpServletRequest request, HttpServletResponse response, String bucketName, String objectName) throws Exception {
        // 判断是否是视频，是否为mp4格式
        String filenameExtension = StringUtils.getFilenameExtension(objectName);
        if (ObjectUtils.isEmpty(filenameExtension) || !"mp4".equalsIgnoreCase(filenameExtension.toLowerCase(Locale.ENGLISH))) {
            throw new IllegalArgumentException("不支持的媒体类型，文件名：" + objectName);
        }
        StatObjectResponse objectInfo = null;
        try {
            objectInfo = MinioUtils.getObjectStat(bucketName, objectName);
        } catch (Exception e) {
            log.error("{}中{}不存在: {}", bucketName, objectName, e.getMessage());
            response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            try {
                Map<String, Object> map = Maps.newHashMap();
                map.put("code", StatusCode.NOT_FOUND.getCode());
                map.put("message", StatusCode.NOT_FOUND.getMessage());
                map.put("data", null);
                response.getWriter().write(JSON.toJSONString(map));
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
            return;
        }

        // Accept-Ranges: bytes
        response.setHeader("Accept-Ranges", "bytes");
        // pos开始读取位置;  last最后读取位置
        long startPos = 0;
        long endPos = objectInfo.size() - 1;
        String rangeHeader = request.getHeader("Range");
        if (!ObjectUtils.isEmpty(rangeHeader) && rangeHeader.startsWith("bytes=")) {
            try {
                // 情景一：RANGE: bytes=2000070- 情景二：RANGE: bytes=2000070-2000970
                String numRang = request.getHeader("Range").replaceAll("bytes=", "");
                if (numRang.startsWith("-")) {
                    startPos = endPos - Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 1, numRang.length() - 1)) + 1;
                } else if (numRang.endsWith("-")) {
                    startPos = Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 0, numRang.length() - 1));
                } else {
                    String[] strRange = numRang.split("-");
                    if (strRange.length == 2) {
                        startPos = Long.parseLong(strRange[0].trim());
                        endPos = Long.parseLong(strRange[1].trim());
                    } else {
                        startPos = Long.parseLong(numRang.replaceAll("-", "").trim());
                    }
                }
                if (startPos < 0 || endPos < 0 || startPos > endPos) {
                    // SC 要求的范围不满足
                    response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                    return;
                }
                // 断点续传 状态码206
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            } catch (NumberFormatException e) {
                log.error(request.getHeader("Range") + " is not Number!");
                startPos = 0;
            }
        }
        // 总共需要读取的字节
        long rangLength = endPos - startPos + 1;
        response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startPos, endPos, objectInfo.size()));
        response.addHeader("Content-Length", String.valueOf(rangLength));
        // response.setHeader("Connection", "keep-alive");
        response.addHeader("Content-Type", "video/mp4");
        try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
             BufferedInputStream bis = new BufferedInputStream(MinioUtils.getObject(bucketName, objectName, startPos, rangLength))) {
            IOUtils.copy(bis, bos);
        } catch (IOException e) {
            if (e instanceof ClientAbortException) {
                // 这里就不要打日志，这里的异常原因是用户在拖拽视频进度造成的
            } else {
                log.error(e.getMessage());
            }
        }
    }

    @Override
    public void videoPlay2(HttpServletRequest request, HttpServletResponse response, String bucketName, String objectName) throws Exception {
        // 判断是否是视频，是否为mp4格式
        String filenameExtension = StringUtils.getFilenameExtension(objectName);
        if (ObjectUtils.isEmpty(filenameExtension) || !"mp4".equalsIgnoreCase(filenameExtension.toLowerCase(Locale.ENGLISH))) {
            throw new IllegalArgumentException("不支持的媒体类型，文件名：" + objectName);
        }
        String range = request.getHeader("Range");
        // 获取文件信息
        StatObjectResponse statObjectResponse = MinioUtils.getObjectStat(bucketName, objectName);
        long startByte = 0;
        long endByte = statObjectResponse.size() - 1;
        log.info("文件开始位置：{}，文件结束位置：{}，文件总长度：{}", startByte, endByte, statObjectResponse.size());
        // 有range的话
        if (StringUtils.hasText(range) && range.contains("bytes=") && range.contains("-")) {
            range = range.substring(range.lastIndexOf("=") + 1).trim();
            String[] ranges = range.split("-");
            try {
                // 判断range的类型
                if (ranges.length == 1) {
                    if (range.startsWith("-")) {// 类型一：bytes=-2343
                        endByte = Long.parseLong(ranges[0]);
                    } else if (range.endsWith("-")) {// 类型二：bytes=2343-
                        startByte = Long.parseLong(ranges[0]);
                    }
                } else if (ranges.length == 2) {// 类型三：bytes=22-2343
                    startByte = Long.parseLong(ranges[0]);
                    endByte = Long.parseLong(ranges[1]);
                }
            } catch (NumberFormatException e) {
                log.error("Range Occur Error, Message:" + e.getLocalizedMessage());
                startByte = 0;
                endByte = statObjectResponse.size() - 1;
            }
        }
        // 要下载的长度
        long contentLength = endByte - startByte + 1;
        // 获取文件类型
        String contentType = request.getServletContext().getMimeType(objectName);
        // 解决下载文件时文件名乱码问题
        objectName = new String(objectName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1);
        // 各种响应头设置
        // 支持断点续传，获取部分字节内容：
        response.setHeader("Accept-Ranges", "bytes");
        // HTTP状态码要为206：表示获取部分内容
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        response.setContentType(contentType);
        response.setHeader("Last-Modified", statObjectResponse.lastModified().toString());
        // inline表示浏览器直接使用，attachment表示下载，filename表示下载的文件名
        response.setHeader("Content-Disposition", "inline;filename=" + objectName);
        response.setHeader("Content-Length", String.valueOf(contentLength));
        // Content-Range格式为：[要下载的开始位置]-[结束位置]/[文件总大小]
        response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + statObjectResponse.size());
        response.setHeader("ETag", "\"".concat(statObjectResponse.etag()).concat("\""));
        try (InputStream stream = MinioUtils.getObject(bucketName, objectName, startByte, contentLength);
             BufferedOutputStream os = new BufferedOutputStream(response.getOutputStream())) {
            int len;
            byte[] buffer = new byte[1024];
            while ((len = stream.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            os.flush();
            response.flushBuffer();
            log.info("下载完毕");
        } catch (ClientAbortException e) {
            log.warn("用户停止下载：" + startByte + "-" + endByte);
        } catch (IOException e) {
            log.error("用户下载IO异常，Message：{}", e.getLocalizedMessage());
        }
    }
}
